:- module(match_interface,
	[
	    start/0
	], [pillow, dcg, assertions, regtypes,basicmodes]).

:- use_module(library(prolog_sys),[statistics/2]).
:- use_module(library(lists),[append/3]).
:- use_module(library(aggregates),[findnsols/4,findall/3]).
:- use_module(library(terms), [atom_concat/2]).
:- use_module(library('pillow/pillow_types')).

:- use_module(search(form_page)).
:- use_module(search(term_desc_page), [generate_term_description/1]).

:- use_module(search(match)).
:- use_module(search(utils)).
:- use_module(amos(configuration), [pers_data_dir/1]).

:- comment(module, "

This module performs the interaction between the user and the matching
engine by means of a web interface based on efficient CGI scripts.
When the web interface starts for the first time, an initial query
with default search options is shown. The user can then select the
description terms to find, and adjust the searching parameters as
well, in order to get more precise results. When the user eventually
performs the query (by pressing the appropiate button on the web
page), assemblies satisfying the query parameters are obtained, and a
result page is shown to the user. The assemblies that result from a
query evaluation are presented to the user together with another
search form, in order to enable incremental searches. The information
introduced by the user is kept in this new form to let him refine the
search.

The underlying implementation of the search web interface is based on
a single HTML form which encodes the application state given by the
@regtype{values/1} procedure. Basically, this procedure abstracts away the
implementation details of the HTML page, mapping each state element as
an HTML input, which can be either hidden, visible or a human readable
menu which disguises the representation of the matching engine
parameters. Depending on whether the application has been started or
not, the main procedure @pred{form_handling/0} executes one of these
actions:

@begin{itemize}

@item [1-] to emit an initial form with default values, or

@item [2-] to receive the CGI input, extract the event identifier
  (represented by an element of type @regtype{id}), input fields and
  previous state encoded as input fields, process them into a new
  state, and generate an updated form.

@end{itemize}

The actions of the web interface which require the Amos matching
engine are @tt{Search}, @tt{Next page} (to show the next page of
search results when the last query resulting assemblies do not fit a
single page) or @tt{Previous page} (to show the previous page of
search results). The connection with the matching engine to retrieve
the search solutions is made by @pred{make_search/2}.  Any other
action simply updates the input state without performing any search.

The search parameters are translated to the internal query language
needed by the matching engine. The translation is straightforward,
with the exception of the parameters to control the search heuristics
(described in @regtype{sort_method/1}).  This interface makes use of
linguistic modifiers to link the meaning of numeric weights (explained
in @tt{match}) in order to make them more intuitive for humans, as
follows:

@begin{itemize}

@item number of packages: very few (-2), few (-1), any number or (0),
  a few (1), many (2)

@item unsatisfied requirements: very few (-2), few (-1), any number or
  (0)

@item auxiliary requirements: very few (-2), few (-1), any number or
  (0)

@item number of capabilities: very few (-2), few (-1), any number or
  (0), a few (1), many (2)

@item ratio of fullfilled capabilities: a very small (-2), a small
  (-1), any (0), a large (1), a very large (2)

@end{itemize}
@newpage
").

%:- comment(doinclude, passwd/1).
%:- comment(doinclude, all).

:- true pred start/0 

# "Main procedure to start the CGI.".

:- comment(doinclude, start/0).
start :-
        % Ensure an error page if anything fails
        html_protect(form_handling).

:- true pred form_handling/0 

# "Gets the CGI input and sends back the corresponding HTML page to
  the client web browser.".

:- comment(doinclude, form_handling/0).
form_handling :-
        log_message('Starting CGI'),
        get_form_input(Info),
        log_message('Info gotten'),
        process_form(Info).

:- true pred process_form(in(Info)) :: form_dict 

# "Generates and sends an HTML page.".

:- comment(doinclude, process_form/1).
process_form(Info) :-
        log_message(processing_form(Info)),
        which_page_id(Info, Id),
        log_message(i_result(Id)),
        decide_page(Id, Info, HtmlCode),
        log_message('Returning HTML code'),
        output_html([cgi_reply, HtmlCode]).

:- export(id/1).
:- regtype id(X) 

# "Identifier of the action.".

:- comment(doinclude, id/1).
id('Description of terms').
id('Add to search').
id('Previous').
id('Next').
id(do_search).
id('').

:- export(values/1).
:- regtype values(X) 

# "Internal state of the search CGI.".

:- comment(values/1,"@includedef{values/1}").
:- comment(doinclude, values/1).
values(values(Terms, Ini, Len, Timeout, SortMethod, Mode, InP, ExP)) :-
	atm(Terms),
	num(Ini),
	num(Len),
	num(Timeout),
	sort_method(SortMethod),
	match_mode(Mode),
	atm(InP),
	atm(ExP).

:- true pred decide_page(in(Id), in(Info), out(HtmlCode)) :: id * form_dict * html_term 

# "Processes form input information @var{Info} and the action
  identifier @var{Id}, and generates @var{HtmlCode}.".

:- comment(doinclude, decide_page/3).
decide_page('Description of terms', _Info, HtmlCode) :- !,
        generate_term_description(HtmlCode).
decide_page(Id, Info, HtmlCode) :-
	( Id = 'Add to search' ->
	    Button = add_terms
	; Id = 'Previous' ->
	    Button = prev_ini
	; Id = 'Next' ->
	    Button = next_ini
	; Id = do_search ->
	    Button = search
	), !,
        log_message('Action'(Button)),
	get_values(Info, Values0),
        log_message('Values0'(Values0)),
	% Update terms from cgi input
        values__get_terms(Values0, Terms02),
	( Button = add_terms ->
	    generate_selected_termnames(TermNames2Add),
	    log_message('Term names added'(TermNames2Add)),
	    get_values_from_multiple_menu(TermNames2Add, Info, Terms2Add),
	    log_message('Selected terms'(Terms2Add)),
	    concat_with_spaces([Terms02|Terms2Add], Terms)
	; Terms = Terms02
	),
	values__replace_terms(Values0, Terms, Values1),
	% Update ini
        values__get_len(Values1, NumOfMatches),
        values__get_ini(Values1, Ini0),
	( Button = prev_ini -> % previous page
	    Ini is Ini0 - NumOfMatches
	; Button = next_ini -> % next page
	    Ini is Ini0 + NumOfMatches
	; % reset Ini
	    Ini = 0
	),
	values__replace_ini(Values1, Ini, Values),
	% Check values
	log_message('Checking values'(Values)),
	check_request(Values, InputErrors),
	log_message('Values checked'),
	( \+ InputErrors = [] ->
	    log_message('Input errors'(InputErrors)),
	    Results = input_errors(InputErrors)
	; ( Button = search ; Button = next_ini ; Button = prev_ini ) ->
	    log_message('No input errors'),
	    log_message('Generating summary results'),
	    ( make_search(Values, Results) ->
	        true
	    ; Results = match_failed
	    )
	; no_results(Results) % do not show any result
	),
	log_message('Generating search form'(Results)),
	generate_form_page(Values, do_search, Results, HtmlCode).
decide_page(_Id, _Info, HtmlCode):-
	% The initial page
        log_message('Generating first page'),
	default_values(Values),
	no_results(Results),
        generate_form_page(Values, do_search, Results, HtmlCode).

:- true pred concat_with_spaces(in(Xs), go(Y)) :: list(atm) * atm 

# "@var{Y} is the concatenation of the elements of @var{Xs} separated
  with blank spaces.".

:- comment(doinclude, concat_with_spaces/2).
concat_with_spaces([],'') :- !.
concat_with_spaces([''|Ts], Result) :- !,
	concat_with_spaces(Ts, Result) .
concat_with_spaces([T|Ts], Result) :-
	concat_with_spaces(Ts, Result1),
	(
            Result1 = '' ->
            Result = T
	;
            atom_concat([T, ' ', Result1], Result)
        ).

values__replace_terms(values(_, Ini, NumOfMatches, Timeout, SortMethod, Mode, InP, ExP), Terms, values(Terms, Ini, NumOfMatches, Timeout, SortMethod, Mode, InP, ExP)).
values__get_terms(values(Terms, _Ini, _NumOfMatches, _Timeout, _SortMethod, _Mode, _InP, _ExP), Terms).

values__replace_ini(values(Terms, _, NumOfMatches, Timeout, SortMethod, Mode, InP, ExP), Ini, values(Terms, Ini, NumOfMatches, Timeout, SortMethod, Mode, InP, ExP)).
values__get_ini(values(_Terms, Ini, _NumOfMatches, _Timeout, _SortMethod, _Mode, _InP, _ExP), Ini).

values__get_len(values(_Terms, _Ini, NumOfMatches, _Timeout, _SortMethod, _Mode, _InP, _ExP), NumOfMatches).

:- true pred default_values(go(Values)) :: values 

# "@var{Values} is the default state of a newly loaded search page.".

:- comment(doinclude, default_values/1).
default_values(values(Terms, Ini, NumOfMatches, Timeout, SortMethod, Mode, InP, ExP)) :-
	Terms = '',
	Ini = 0,
	NumOfMatches = 10,
	Timeout = 10,
	SortMethod = key_weights(
	       WPackageNumber, 
	       WUnsatisfiedDeps,
	       WTighterAssemblies,
	       WCapabilities,    
	       WBestCapabilitiesRatio),
	Mode = gen(LevelPrec, LevelPost),
	WPackageNumber = 0, 
	WUnsatisfiedDeps = 0,
	WTighterAssemblies = 0,
	WCapabilities = 0,
	WBestCapabilitiesRatio = 0,
	LevelPrec = 0,
	LevelPost = 0,
	InP = '',
	ExP = ''.

:- true pred get_values(in(Info), go(Values)) :: form_dict * values 

# "Decodes the information @var{Info} obtained by the CGI from the input form and returns it in @var{Values}.".

:- comment(doinclude, get_values/2).
get_values(Info, values(Terms, Ini, NumOfMatches, Timeout, SortMethod, Mode, InP, ExP)) :-
        get_form_value(Info, search_terms, Terms0),
        get_form_value(Info, ini, Ini),
        get_form_value(Info, timeout, Timeout0),
	get_form_value(Info, include_pkgs, InP0),
	get_form_value(Info, exclude_pkgs, ExP0),
        get_form_value(Info, search_levelprec, LevelPrec),
        get_form_value(Info, search_levelpost, LevelPost),
        get_form_value(Info, num_matches, NumOfMatches),
	get_form_value(Info, wpackagenumber, WPackageNumber),
	get_form_value(Info, wunsatisfieddeps, WUnsatisfiedDeps),
	get_form_value(Info, wtighterassemblies, WTighterAssemblies),
	get_form_value(Info, wcapabilities, WCapabilities),
	get_form_value(Info, wbestcapabilitiesratio, WBestCapabilitiesRatio),
	Mode = gen(LevelPrec, LevelPost),
	SortMethod = key_weights(
	       WPackageNumber, 
	       WUnsatisfiedDeps,
	       WTighterAssemblies,
	       WCapabilities,    
	       WBestCapabilitiesRatio),
	( Terms0 = '$empty' -> Terms = '' ; to_lowercase(Terms0, Terms) ),
	( InP0 = '$empty' -> InP = '' ; InP = InP0 ),
	( ExP0 = '$empty' -> ExP = '' ; ExP = ExP0 ),
	( Timeout0 > 60 -> Timeout = 60 ; Timeout = Timeout0 ).

:- true pred no_results(go(Results)) :: results # "@var{Results} is an empty result".
:- comment(doinclude, no_results/1).
no_results(no_results).

:- export(results/1).

:- regtype results/1.

:- comment(results(Results), "A list of assemblies which are the
   result of a search, plus the total number of solutions @var{Total}
   and the computation time @var{Time}.").
:- comment(doinclude, results/1).

results(results(MatchList, Total, Time)) :-
	match_sort_result(MatchList),
	num(Total),
	num(Time).

:- true pred make_search(in(Values), go(Results)) :: values * results

# "Converts the search parameters @var{Values} stored in the 
   CGI state to the internal query language and performs the query.".

%:- comment(make_search/2, "Given the tuple @tt{values(Terms, Ini,
%   NumOfMatches, SortMethod, Mode, InPA, ExPA)}, this procedure splits
%   the atom @tt{Terms} into a list of capability names, the atom
%   @tt{InPA} into a list of package names and the atom @tt{ExPA} into
%   a list of package names. The rest of parameters needs no decoding.
%   The computation time is measured using @pred{statistics/2}.").

:- comment(doinclude, make_search/2).

make_search(Values, results(MatchList, Total, Time)):-
        Values = values(Terms, Ini, NumOfMatches, Timeout, SortMethod, Mode, InPA, ExPA), !,
	decode_package_list(InPA, InP), 
	decode_package_list(ExPA, ExP),
	atom_codes(Terms, TermsAsString),
	split_into_atoms(TermsAsString, AtomTerms),

	statistics(walltime, [T0|_]),

	Timeout2 is Timeout * 1000, % seconds to milliseconds
	match_sort_interval_list(SortMethod, Mode, AtomTerms, Timeout2, InP, ExP, Ini, NumOfMatches, MatchList, Total),

        statistics(walltime, [T1|_]),
	Time is T1 - T0.

:- true pred decode_package_list(in(PA), go(P)) :: atm * list(package)
   # "@var{P} is the result of decoding @var{PA}.".
:- comment(doinclude, decode_package_list/2).
decode_package_list(PA, P) :-
	( PA = '$empty' ->
            P = []
	;
            atom_codes(PA, PStr),
            split_into_atoms(PStr, P)
	).

:- true pred check_request(in(Values), go(Problems)) :: values *
   list(problem) # "Checks the types of @var{Values} and generate a the
   possible list of problems @var{Problems}".
:- comment(doinclude, check_request/2).
check_request(Values, Problems) :-
	check_request__2(Values, Problems, []).

:- true pred check_request__2(in(Values), go(P0), in(P)) :: values *
   list(problem) * list(problem).
check_request__2(Values) -->
	{ Values = values(Terms, Ini, NumOfMatches, Timeout, SortMethod, Mode, _InP, _ExP) },
	{ Mode = gen(LevelPrec, LevelPost) },
	{ SortMethod = key_weights(
	       WPackageNumber, 
	       WUnsatisfiedDeps,
	       WTighterAssemblies,
	       WCapabilities,    
	       WBestCapabilitiesRatio)
	},
	check_request__empty(terms, Terms),
	check_request__number(ini, Ini),
	check_request__number(num_match, NumOfMatches),
	check_request__number(timeout, Timeout),
	check_request__number(wpackagenumber, WPackageNumber),
	check_request__number(wunsatisfieddeps, WUnsatisfiedDeps),
	check_request__number(wtighterassemblies, WTighterAssemblies),
	check_request__number(wcapabilities, WCapabilities),
	check_request__number(wbestcapabilitiesratio, WBestCapabilitiesRatio),
	check_request__number(levelprec, LevelPrec),
	check_request__number(levelpost, LevelPost).
        
:- true pred check_request__number(in(Name), in(Value), go(P0), in(P))
   :: atm * atm * list(problem) * list(problem) # "Inserts the
   corresponding problem (identifing the culprit input @var{Name}) into
   the difference list @var{P0}-@var{P} if the type of @var{Value} is
   not a number.".
:- comment(doinclude, check_request__number/4).
check_request__number(Name, Value) -->
        ( { \+ number(Value) } ->
	    [no_num(Name)],
	    { log_message(no_num(Name)) }
	; []
	).

:- true pred check_request__empty(in(Name), in(Value), go(P0), in(P))
   :: atm * atm * list(problem) * list(problem) # "Insert the
   corresponding problem (identifing the culprit input @var{Name}) into
   the difference list @var{P0}-@var{P} if the type of @var{Value} is
   empty.".
:- comment(doinclude, check_request__empty/4).
check_request__empty(Name, Value) -->
        ( { Value = '$empty' ; Value = '' } ->
	    [empty(Name)],
	    { log_message(empty(Name)) }
	; []
	).

:- true pred which_page_id(in(Info), go(Id)) :: form_dict * id #
   "Obtains the identifier of the current page @var{Id} from
   @var{Info}. This identifier can be blank if the form is in its
   initial state or a particular value telling which button or action
   was selected by the user.".
:- comment(doinclude, which_page_id/2).
which_page_id(Info, Id):-
        get_form_value(Info, page_id, PageId),
        get_form_value(Info, term_description_page, IsDescription),
        get_form_value(Info, add_to_search, Add),
        get_form_value(Info, next_ini, Next),
        get_form_value(Info, prev_ini, Prev),
	get_form_value(Info, revert, Revert),
	Actions = [PageId, IsDescription, Add, Next, Prev, Revert],
	log_message(actions_required(Actions)),
	set_action(Actions, Id).

:- true pred set_action(in(As), go(Id)) :: list(atm) * atm.
set_action([A], A).
set_action([A|As], Id) :-
	set_action(As, Id1),
	( Id1 = '' ->
	    Id = A
	; Id = Id1
	).

:- true pred get_values_from_multiple_menu(Fields, Info, ValuesList)
   :: list(atm) * form_dict * list(constant) # ".".
get_values_from_multiple_menu([],_,[]).
get_values_from_multiple_menu([Field|Fs], Info, ValuesList) :-
	findall(Value, member(Field = Value, Info), Values),

	append(Values, RVs, ValuesList),
	get_values_from_multiple_menu(Fs, Info, RVs).
